package com.test.interceptor;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import com.test.common.Page;


/**
 * <p>Title: PageInterceptor </p>
 * <p>Description: mybatis 分页拦截器</p>
 * <p>Company: cse.hnust</p> 
 * @author liuwz
 * @date  2017年1月4日 下午7:30:23
 */

@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
public class PageInterceptor implements Interceptor{
	/**
	 * 数据库类型
	 */
	private String dialect;

	/**
	 * invocation:有被拦截下来的对象，也就是实现了StatementHandler接口的对象
	 * 判断对象是否需要分页操作的业务
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
		/**
		 * 这里我规定凡是在*mapper.xml中查询语句的id是以ByPage结尾的
		 * 都认为它需要分页操作的业务
		 *    最开始拦截的是RoutingStatementHandler
		 * 在BaseStatementHandler类中引入了类MappedStatement，MappedStatement mappedStatement
		 *   而在MappedStatement类中有很多属性，其中id就是对应*mapper.xml中sql语句的id
		 * 由于MappedStatement是受保护的( protected final MappedStatement mappedStatement;)
		 *   所以要通过反射拿到id
		 * mybatis中MetaObject已经封装了对反射的实现
		 */
		MetaObject metaObject = MetaObject.forObject(statementHandler, 
				SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY);
		MappedStatement mappedStatement = (MappedStatement)metaObject.
				getValue("delegate.mappedStatement");
		//拿到配置文件中SQL语句的id
		String id = mappedStatement.getId();
		/**
		 * 正则表达式
		 * . 代表前面至少有任意一个字符
		 * $ 代表字符串的结束符
		 */
		if(id.matches(".+ByPage$")) {
			/**
			 * 拿到SQL语句，BoundSql中就有SQL语句
			 * 通过statementHandler的get方法
			 */
			BoundSql boundSql =statementHandler.getBoundSql();
			// 原始的SQL语句
			String sql = boundSql.getSql();
			// 拿到参数
			Map<?, ?> param = (Map<?, ?>) boundSql.getParameterObject();
			Page page = (Page) param.get("page");
			
			/*	
			 * 	// 1.1查询总条数
			 */ 
			String countSql = "select count(1) from ("+ sql +")a";
			// 1.2invocation.getArgs() 得到参数数组 由于只有一个参数为 Connection.class,所以取[0]
			Connection connection = (Connection)invocation.getArgs()[0];
			
			PreparedStatement coutStatement = connection.prepareStatement(countSql);
			/**
			 * 原sql语句中#{}都被解析成了？，所以要手动设置参数
			 * ParameterHandler的setParameters(Statement)方法负责 根据我们输入的参数，
			 * 对statement对象的 ? 占位符处进行赋值。
			 */
			ParameterHandler parameterHandler =(ParameterHandler)metaObject.getValue("delegate.parameterHandler");
			parameterHandler.setParameters(coutStatement);
			// 1.3手动执行sql
			ResultSet rs = coutStatement.executeQuery();
			if(rs.next()) {
				page.setTotalRow(rs.getLong(1));
			}
			
			// 2.1改造带分页的SQL语句
			String pageSql = getPageSql(sql, page.getPageSize(), page.getPageStart());
			// 2.2修改属性delegate.boundSql.sql的值为新的pageSql
			metaObject.setValue("delegate.boundSql.sql", pageSql);
		}
		    // 2.3拼接完成后交回主权
		//proceed()方法：就是一个反射，用反射来调用被拦截住的instantiateStatement()方法，返回值就是Statement
		return invocation.proceed();
		
	}

	/**
	 * 功能：拦截对象
	 * 不需要代理，就放了此处拦截的对象，返回的是被拦截的对象
	 * 需要代理，则达成规定协议，返回的是具有代理权力的代理类
	 */
	@Override
	public Object plugin(Object target) {
		/*当目标类是StatementHandler类型时，才包装目标类，否者直接返回目标本身,减少目标被代理的次数
		  this:分页拦截器类的实例，形象说它本来只是普通的业务员
		     经过Plugin.wrap(target, this)，this变成了有代理权力的代理员
		*/
            return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
		this.dialect = properties.getProperty("dialect");
	}
	
	/**
	 * 得到改造后的分页sql，支持mysql和oracle
	 * @param sql         原始sql
	 * @param pageSize    分页大小
	 * @param pageStart   第几条开始分页(0代表第一条)
	 * @return            pageSql
	 */
	private String getPageSql(String sql, int pageSize, int pageStart) {
		if("mysql".equalsIgnoreCase(dialect)) {
			return getMysqlPageSql(sql, pageSize, pageStart);
		}
		if("oracle".equalsIgnoreCase(dialect)) {
			return getOraclePageSql(sql, pageSize, pageStart);
		}
		return sql;
	}
	
	/**
	 * mysql分页语句改造
	 * @param sql
	 * @param pageSize
	 * @param pageStart
	 * @return
	 */
	private String getMysqlPageSql(String sql, int pageSize, int pageStart) {
		StringBuffer sqlBuffer = new StringBuffer();
		sqlBuffer.append(sql);
		sqlBuffer.append(" limit ").append(pageStart).append(",").append(pageSize);
		return sqlBuffer.toString();
	}
	
	/**
	 * oracle分页语句改造
	 * @param sql
	 * @param pageSize
	 * @param pageStart
	 * @return
	 */
	private String getOraclePageSql(String sql, int pageSize, int pageStart) {
		StringBuffer sqlBuffer = new StringBuffer();
		int pageEnd = pageSize + pageStart + 1;
		sqlBuffer.append(sql);
		sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(pageEnd);
		sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(pageStart); 
		//select * from (select u.*, rownum r from (select * from user) u where rownum < 11) where r >= 0  
		return sqlBuffer.toString();
	}
	
}
